{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "269ce58d",
   "metadata": {},
   "source": [
    "# Branch Creation for Parallel Node Execution\n",
    "\n",
    "- Author: [seofield](https://github.com/seofield)\n",
    "- Peer Review: \n",
    "- Proofread : [Chaeyoon Kim](https://github.com/chaeyoonyunakim)\n",
    "- This is a part of [LangChain Open Tutorial](https://github.com/LangChain-OpenTutorial/LangChain-OpenTutorial)\n",
    "\n",
    "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/LangChain-OpenTutorial/LangChain-OpenTutorial/blob/main/17-LangGraph/01-Core-Features/11-LangGraph-Branching.ipynb)[![Open in GitHub](https://img.shields.io/badge/Open%20in%20GitHub-181717?style=flat-square&logo=github&logoColor=white)](https://github.com/LangChain-OpenTutorial/LangChain-OpenTutorial/blob/main/17-LangGraph/01-Core-Features/11-LangGraph-Branching.ipynb)\n",
    "## Overview\n",
    "\n",
    "Parallel execution of nodes is essential for improving the overall performance of graph-based workflows. LangGraph provides native support for parallel node execution, significantly enhancing the efficiency of workflows built with this framework.\n",
    "\n",
    "This parallelization is achieved using **fan-out** and **fan-in** mechanisms, utilizing both standard edges and ```conditional_edges```.\n",
    "\n",
    "![branching-graph](./assets/11-langgraph-branching-graph.png)\n",
    "\n",
    "### Table of Contents\n",
    "\n",
    "- [Overview](#overview)\n",
    "- [Parallel Node Fan-out and Fan-in](#parallel-node-fan-out-and-fan-in)\n",
    "- [Fan-out and Fan-in of Parallel Nodes with Additional Steps](#fan-out-and-fan-in-of-parallel-nodes-with-additional-steps)\n",
    "- [Conditional Branching](#conditional-branching)\n",
    "- [Sorting Based on Reliability of Fan-out Values](#sorting-based-on-reliability-of-fan-out-values)\n",
    "\n",
    "### References\n",
    "\n",
    "- [How to create branches for parallel node execution](https://langchain-ai.github.io/langgraph/how-tos/branching/)\n",
    "----"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0125f25f",
   "metadata": {},
   "source": [
    "## Environment Setup\n",
    "\n",
    "Setting up your environment is the first step. See the [Environment Setup](https://wikidocs.net/257836) guide for more details.\n",
    "\n",
    "\n",
    "**[Note]**\n",
    "\n",
    "The langchain-opentutorial is a package of easy-to-use environment setup guidance, useful functions and utilities for tutorials.\n",
    "Check out the  [```langchain-opentutorial```](https://github.com/LangChain-OpenTutorial/langchain-opentutorial-pypi) for more details."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "b352a8fe",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%capture --no-stderr\n",
    "%pip install langchain-opentutorial"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "f778b21d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Install required packages\n",
    "from langchain_opentutorial import package\n",
    "\n",
    "package.install(\n",
    "    [\n",
    "        \"langchain\",\n",
    "    ],\n",
    "    verbose=False,\n",
    "    upgrade=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ff3a1c12",
   "metadata": {},
   "source": [
    "You can set API keys in a ```.env``` file or set them manually.\n",
    "\n",
    "[Note] If you’re not using the ```.env``` file, no worries! Just enter the keys directly in the cell below, and you’re good to go."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "2e6bb264",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Environment variables have been set successfully.\n"
     ]
    }
   ],
   "source": [
    "from dotenv import load_dotenv\n",
    "from langchain_opentutorial import set_env\n",
    "\n",
    "# Attempt to load environment variables from a .env file; if unsuccessful, set them manually.\n",
    "if not load_dotenv():\n",
    "    set_env(\n",
    "        {\n",
    "            \"OPENAI_API_KEY\": \"\",\n",
    "            \"LANGCHAIN_API_KEY\": \"\",\n",
    "            \"LANGCHAIN_TRACING_V2\": \"false\",\n",
    "            \"LANGCHAIN_ENDPOINT\": \"https://api.smith.langchain.com\",\n",
    "            \"LANGCHAIN_PROJECT\": \"\",  # set the project name same as the title\n",
    "        }\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2a87109f",
   "metadata": {},
   "source": [
    "## Parallel Node Fan-out and Fan-in\n",
    "\n",
    "**Fan-out / Fan-in**\n",
    "\n",
    "In parallel processing, **fan-out** and **fan-in** describe the processes of dividing and consolidating tasks.\n",
    "\n",
    "- **Fan-out (Expansion)**: A large task is divided into smaller, more manageable tasks. For example, when making a pizza, the dough, sauce, and cheese can be prepared independently. Dividing tasks to process them simultaneously is fan-out.\n",
    "\n",
    "- **Fan-in (Consolidation)**: The divided smaller tasks are brought together to complete the overall task. Just like assembling the prepared ingredients to create a finished pizza, fan-in collects the results of parallel tasks to finalize the process.\n",
    "\n",
    "In essence, **fan-out** distributes tasks, and **fan-in** gathers the results to produce the final output.\n",
    "\n",
    "\n",
    "This example illustrates a fan-out from ```Node A``` to ```Node B``` and ```Node C```, followed by a fan-in to ```Node D```.\n",
    "\n",
    "In the **State**, the ```reducer(add)``` operator is specified. This ensures that instead of simply overwriting existing values for a specific key in the State, the values are combined or accumulated. For lists, this means appending the new list to the existing one.\n",
    "\n",
    "LangGraph uses the ```Annotated``` type to specify reducer functions for specific keys in the State. This approach allows attaching a reducer function (e.g., ```add```) to the type without changing the original type (e.g., ```list```) while maintaining compatibility with type checking."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "0da53871",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Annotated, Any\n",
    "from typing_extensions import TypedDict\n",
    "from langgraph.graph import StateGraph, START, END\n",
    "from langgraph.graph.message import add_messages\n",
    "\n",
    "\n",
    "# Define State (using add_messages reducer)\n",
    "class State(TypedDict):\n",
    "    aggregate: Annotated[list, add_messages]\n",
    "\n",
    "\n",
    "# Class for returning node values\n",
    "class ReturnNodeValue:\n",
    "    # Initialization\n",
    "    def __init__(self, node_secret: str):\n",
    "        self._value = node_secret\n",
    "\n",
    "    # Updates the state when called\n",
    "    def __call__(self, state: State) -> Any:\n",
    "        print(f\"Adding {self._value} to {state['aggregate']}\")\n",
    "        return {\"aggregate\": [self._value]}\n",
    "\n",
    "\n",
    "# Initialize the state graph\n",
    "builder = StateGraph(State)\n",
    "\n",
    "# Create nodes A through D and assign values\n",
    "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
    "builder.add_edge(START, \"a\")\n",
    "builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n",
    "builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n",
    "builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n",
    "\n",
    "# Connect the nodes\n",
    "builder.add_edge(\"a\", \"b\")\n",
    "builder.add_edge(\"a\", \"c\")\n",
    "builder.add_edge(\"b\", \"d\")\n",
    "builder.add_edge(\"c\", \"d\")\n",
    "builder.add_edge(\"d\", END)\n",
    "\n",
    "# Compile the graph\n",
    "graph = builder.compile()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3e3ba6e8",
   "metadata": {},
   "source": [
    "Let's visualize the graph."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "89b5086c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6aff4a26",
   "metadata": {},
   "source": [
    "You can observe that the values added by each node are **accumulated** through the ```reducer```."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "64a87798",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding I'm A to []\n",
      "Adding I'm B to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='04834ec3-a39e-4713-9662-12bda90f5acf')]\n",
      "Adding I'm C to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='04834ec3-a39e-4713-9662-12bda90f5acf')]\n",
      "Adding I'm D to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='04834ec3-a39e-4713-9662-12bda90f5acf'), HumanMessage(content=\"I'm B\", additional_kwargs={}, response_metadata={}, id='54171388-7830-40a2-b130-46c4d0d0b38b'), HumanMessage(content=\"I'm C\", additional_kwargs={}, response_metadata={}, id='9887bbbc-e0d3-4e5e-8e52-a6201e044ad2')]\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'aggregate': [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='04834ec3-a39e-4713-9662-12bda90f5acf'),\n",
       "  HumanMessage(content=\"I'm B\", additional_kwargs={}, response_metadata={}, id='54171388-7830-40a2-b130-46c4d0d0b38b'),\n",
       "  HumanMessage(content=\"I'm C\", additional_kwargs={}, response_metadata={}, id='9887bbbc-e0d3-4e5e-8e52-a6201e044ad2'),\n",
       "  HumanMessage(content=\"I'm D\", additional_kwargs={}, response_metadata={}, id='acd59483-71f2-416d-8ec6-65be0cee82ea')]}"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Execute the Graph\n",
    "graph.invoke({\"aggregate\": []}, {\"configurable\": {\"thread_id\": \"foo\"}})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3ccf5a22",
   "metadata": {},
   "source": [
    "### Handling Exceptions during Parallel Processing\n",
    "\n",
    "LangGraph executes nodes within a \"superstep\". This means that even if parallel branches are executed simultaneously, the entire superstep is processed in a **transactional** manner.\n",
    "\n",
    "> **Superstep**: A complete processing step involving multiple nodes.\n",
    "\n",
    "As a result, if an exception occurs in any of the branches, **no updates** are applied to the state (the entire superstep is rolled back).\n",
    "\n",
    "![branching-graph](./assets/11-langgraph-branching-graph.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "af5ad6dd",
   "metadata": {},
   "source": [
    "For tasks prone to errors (e.g., handling unreliable API calls), LangGraph offers two solutions:\n",
    "\n",
    "1. You can write standard Python code within nodes to catch and handle exceptions directly.\n",
    "2. Set up a **[retry_policy](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.graph.CompiledGraph.retry_policy)** to instruct the graph to retry nodes that encounter specific types of exceptions. Only the failed branches are retried, so you don’t need to worry about unnecessary reprocessing.\n",
    "\n",
    "These features enable complete control over parallel execution and exception handling."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f501c7df",
   "metadata": {},
   "source": [
    "## Fan-out and Fan-in of Parallel Nodes with Additional Steps\n",
    "\n",
    "The previous example demonstrated how to perform ```fan-out``` and ```fan-in``` when each path consists of a single step. But what happens when a path contains multiple steps?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "0b0ab72a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Annotated\n",
    "from typing_extensions import TypedDict\n",
    "from langgraph.graph import StateGraph, START, END\n",
    "from langgraph.graph.message import add_messages\n",
    "\n",
    "\n",
    "# Define State (using add_messages reducer)\n",
    "class State(TypedDict):\n",
    "    aggregate: Annotated[list, add_messages]\n",
    "\n",
    "\n",
    "# Class for returning node values\n",
    "class ReturnNodeValue:\n",
    "    # Initialization\n",
    "    def __init__(self, node_secret: str):\n",
    "        self._value = node_secret\n",
    "\n",
    "    # Updates the state when called\n",
    "    def __call__(self, state: State) -> Any:\n",
    "        print(f\"Adding {self._value} to {state['aggregate']}\")\n",
    "        return {\"aggregate\": [self._value]}\n",
    "\n",
    "\n",
    "# Initialize the state graph\n",
    "builder = StateGraph(State)\n",
    "\n",
    "# Create and connect nodes\n",
    "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
    "builder.add_edge(START, \"a\")\n",
    "builder.add_node(\"b1\", ReturnNodeValue(\"I'm B1\"))\n",
    "builder.add_node(\"b2\", ReturnNodeValue(\"I'm B2\"))\n",
    "builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n",
    "builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n",
    "builder.add_edge(\"a\", \"b1\")\n",
    "builder.add_edge(\"a\", \"c\")\n",
    "builder.add_edge(\"b1\", \"b2\")\n",
    "builder.add_edge([\"b2\", \"c\"], \"d\")\n",
    "builder.add_edge(\"d\", END)\n",
    "\n",
    "# Compile the graph\n",
    "graph = builder.compile()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "abbfdf81",
   "metadata": {},
   "source": [
    "Let's visualize the graph."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "b6abb4f4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "bcd2d8ad",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding I'm A to []\n",
      "Adding I'm B1 to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='e0e0b30d-2611-41ec-a735-0e8835b45205')]\n",
      "Adding I'm C to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='e0e0b30d-2611-41ec-a735-0e8835b45205')]\n",
      "Adding I'm B2 to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='e0e0b30d-2611-41ec-a735-0e8835b45205'), HumanMessage(content=\"I'm B1\", additional_kwargs={}, response_metadata={}, id='00c18d89-67b8-4163-9783-7e20bfb73059'), HumanMessage(content=\"I'm C\", additional_kwargs={}, response_metadata={}, id='30287806-99a5-4d83-96d5-5c350bc618a8')]\n",
      "Adding I'm D to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='e0e0b30d-2611-41ec-a735-0e8835b45205'), HumanMessage(content=\"I'm B1\", additional_kwargs={}, response_metadata={}, id='00c18d89-67b8-4163-9783-7e20bfb73059'), HumanMessage(content=\"I'm C\", additional_kwargs={}, response_metadata={}, id='30287806-99a5-4d83-96d5-5c350bc618a8'), HumanMessage(content=\"I'm B2\", additional_kwargs={}, response_metadata={}, id='36d71515-3960-48ee-ab9a-e4482c33090f')]\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'aggregate': [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='e0e0b30d-2611-41ec-a735-0e8835b45205'),\n",
       "  HumanMessage(content=\"I'm B1\", additional_kwargs={}, response_metadata={}, id='00c18d89-67b8-4163-9783-7e20bfb73059'),\n",
       "  HumanMessage(content=\"I'm C\", additional_kwargs={}, response_metadata={}, id='30287806-99a5-4d83-96d5-5c350bc618a8'),\n",
       "  HumanMessage(content=\"I'm B2\", additional_kwargs={}, response_metadata={}, id='36d71515-3960-48ee-ab9a-e4482c33090f'),\n",
       "  HumanMessage(content=\"I'm D\", additional_kwargs={}, response_metadata={}, id='c3c583c6-a496-4f39-a112-c90d41849fa1')]}"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Execute Graph Aggregation with an Empty List, perform a basic aggregation operation across all data using an empty list as the initial state.\n",
    "graph.invoke({\"aggregate\": []})\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8b35570b",
   "metadata": {},
   "source": [
    "## Conditional Branching\n",
    "\n",
    "When the fan-out is non-deterministic, you can directly use ```add_conditional_edges```."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "93354095",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Annotated, Sequence\n",
    "from typing_extensions import TypedDict\n",
    "from langgraph.graph import END, START, StateGraph\n",
    "from langgraph.graph.message import add_messages\n",
    "\n",
    "\n",
    "# Define State (using add_messages reducer)\n",
    "class State(TypedDict):\n",
    "    aggregate: Annotated[list, add_messages]\n",
    "    which: str\n",
    "\n",
    "\n",
    "# Class for returning unique values per node\n",
    "class ReturnNodeValue:\n",
    "    def __init__(self, node_secret: str):\n",
    "        self._value = node_secret\n",
    "\n",
    "    def __call__(self, state: State) -> Any:\n",
    "        print(f\"Adding {self._value} to {state['aggregate']}\")\n",
    "        return {\"aggregate\": [self._value]}\n",
    "\n",
    "\n",
    "# Initialize the state graph\n",
    "builder = StateGraph(State)\n",
    "\n",
    "# Define nodes and connect them\n",
    "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
    "builder.add_edge(START, \"a\")\n",
    "builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n",
    "builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n",
    "builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n",
    "builder.add_node(\"e\", ReturnNodeValue(\"I'm E\"))\n",
    "\n",
    "\n",
    "# Define the routing logic based on the 'which' value in the state\n",
    "def route_bc_or_cd(state: State) -> Sequence[str]:\n",
    "    if state[\"which\"] == \"cd\":\n",
    "        return [\"c\", \"d\"]\n",
    "    return [\"b\", \"c\"]\n",
    "\n",
    "\n",
    "# List of nodes to process in parallel\n",
    "intermediates = [\"b\", \"c\", \"d\"]\n",
    "\n",
    "builder.add_conditional_edges(\n",
    "    \"a\",\n",
    "    route_bc_or_cd,\n",
    "    intermediates,\n",
    ")\n",
    "\n",
    "for node in intermediates:\n",
    "    builder.add_edge(node, \"e\")\n",
    "\n",
    "\n",
    "# Connect the final node and compile the graph\n",
    "builder.add_edge(\"e\", END)\n",
    "graph = builder.compile()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9f69fcdf",
   "metadata": {},
   "source": [
    "If there is a known \"sink\" node to connect to after the conditional branching, you can specify ```then=\"node_name_to_execute\"``` when creating the conditional edge.\n",
    "\n",
    "Here is a reference code snippet. When using the ```then``` syntax, you can add ```then=\"e\"``` and omit adding explicit edge connections."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "e718ebeb",
   "metadata": {},
   "outputs": [],
   "source": [
    "## Using the `then` Syntax\n",
    "# builder.add_conditional_edges(\n",
    "#     \"a\",\n",
    "#     route_bc_or_cd,\n",
    "#     intermediates,\n",
    "#     then=\"e\",\n",
    "# )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3400d24c",
   "metadata": {},
   "source": [
    "Let's visualize the graph."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "9ac61928",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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Uv3www8zZ850Op1UbhcJP7nke/LkyQ0bNlB/eZ/qM+QKhSIvL++pp55i/IUJf7jku2vXrm+//XbZsmXUbxrBpZ2AgICioqLXX3/92LFj1G8dI8uaNWtOnz793XffIdk6smuS+fn527Zt++GHH1AVABuz7/J9++23eTzeu+++i6oAlBfTP//88+rqagqusiDB1Lv02FTFAAARpElEQVR8jUbj3Llz+/TpM2PGDIRlIJ4FMm/evPT09OHDh9fVUbf6OfbQjhw5MmLEiFdffRX52SH0j3fJyspKTU3Nzc196aWXsrKyUJdDGubNDluxYsXt27d/++031IUA9D2uR3h4+O7du4uKit577z3UtZCGSbPDrFbr1KlTQ0NDfWe+lE8E12Pp0qVdunSZO3duWVkZ6lpIEBQUxIyDs3379r322mtvvfVWTk4O6lr+gn6ocKfRo0f36NHjtddeGzRoENqx/6NrbGxkwMHZggUL2Gz26tWrURdyNx/qcT0iIyO3bt3qcDhycnJofcQmk8m4XN/qFx7I8ePHn3zyyQEDBrz//vuoa2mFj+7Z2bNn9+vXLzc3d9q0adRMkyOd0Wik75Xt5cuX19fXHz161GevWvtcj9uiQ4cOu3fvrqysfO211+jY9UokEjpOazx58uScOXM6duy4cuVKn02t7/a4Ld58880LFy7k5uaOHz9++vTpqMt5AGazmXbTGhcvXqzVapctWxYQEIC6ljb4bo/bIiUlZffu3WazecKECTR6wIRKpRIKYT1+kXS//PJLWlpaenr6qlWrfD+1NOhxW7zyyivDhg1btGhRWlravHnzUJfTNo1GExsbi7qKthkMhoULF8pkstOnT9PoBk9vnyzpOzZt2vTrr79OmzatT58+qGtpxdixYysrK1vu8iUIgiCITp06UTzP2ksbNmw4c+ZMdnb2U089hbqWB0ODocJdpkyZ8vHHH//000/z5s1rbKTuYd5e6tu3L4vFaum6WCyWUql8/vnnUdd1t3Pnzo0dO1an061cuZJ2qaVlj9uisLBw2bJl2dnZubm5qGv5S319/UsvveTpdD26d+/+1VdfIS3qfzgcjvfee6+2tnbhwoW0GMy0in49bovMzMz9+/fbbLYRI0acPXsWdTl/Cg0NvXMpY4VC8cwzzyCt6H/s2rWrd+/e6enp33zzDX1TS+/gerz00ktfffXV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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "6b961327",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding I'm A to []\n",
      "Adding I'm B to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='da51b3ce-e49c-422f-aac1-ac1a4c620a50')]\n",
      "Adding I'm C to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='da51b3ce-e49c-422f-aac1-ac1a4c620a50')]\n",
      "Adding I'm E to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='da51b3ce-e49c-422f-aac1-ac1a4c620a50'), HumanMessage(content=\"I'm B\", additional_kwargs={}, response_metadata={}, id='4da170a0-c01d-476c-807f-5de7307aafcc'), HumanMessage(content=\"I'm C\", additional_kwargs={}, response_metadata={}, id='c43a6e78-7096-44ae-9041-2a9e5278db68')]\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'aggregate': [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='da51b3ce-e49c-422f-aac1-ac1a4c620a50'),\n",
       "  HumanMessage(content=\"I'm B\", additional_kwargs={}, response_metadata={}, id='4da170a0-c01d-476c-807f-5de7307aafcc'),\n",
       "  HumanMessage(content=\"I'm C\", additional_kwargs={}, response_metadata={}, id='c43a6e78-7096-44ae-9041-2a9e5278db68'),\n",
       "  HumanMessage(content=\"I'm E\", additional_kwargs={}, response_metadata={}, id='9976cd5c-dfdf-412d-8a50-833d26c864bd')],\n",
       " 'which': 'bc'}"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Execute the Graph (set `which`:`bc`)\n",
    "graph.invoke({\"aggregate\": [], \"which\": \"bc\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "e4877888",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding I'm A to []\n",
      "Adding I'm C to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='f19f377c-03a3-43ca-9abc-755babeada1d')]\n",
      "Adding I'm D to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='f19f377c-03a3-43ca-9abc-755babeada1d')]\n",
      "Adding I'm E to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='f19f377c-03a3-43ca-9abc-755babeada1d'), HumanMessage(content=\"I'm C\", additional_kwargs={}, response_metadata={}, id='cbadef64-f5ac-414f-89bd-d7de7fbf8f36'), HumanMessage(content=\"I'm D\", additional_kwargs={}, response_metadata={}, id='285ae8b7-7b9b-4076-b3b1-35ce66a62ef1')]\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'aggregate': [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='f19f377c-03a3-43ca-9abc-755babeada1d'),\n",
       "  HumanMessage(content=\"I'm C\", additional_kwargs={}, response_metadata={}, id='cbadef64-f5ac-414f-89bd-d7de7fbf8f36'),\n",
       "  HumanMessage(content=\"I'm D\", additional_kwargs={}, response_metadata={}, id='285ae8b7-7b9b-4076-b3b1-35ce66a62ef1'),\n",
       "  HumanMessage(content=\"I'm E\", additional_kwargs={}, response_metadata={}, id='8abc9b73-6fe8-42a7-b04b-aa30a25fbd4b')],\n",
       " 'which': 'cd'}"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Execute the Graph (set `which`:`cd`)\n",
    "graph.invoke({\"aggregate\": [], \"which\": \"cd\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0c93638e",
   "metadata": {},
   "source": [
    "## Sorting Based on Reliability of Fan-out Values\n",
    "\n",
    "Nodes spread out in parallel are executed as part of a single \"**super-step**\". Updates from each super-step are sequentially applied to the state only after the super-step is completed.\n",
    "\n",
    "If a consistent, predefined order of updates is required during a parallel super-step, the output values can be recorded in a separate field of the state with an identifying key. Then, use standard ```edges``` from each fan-out node to the convergence point, where a \"sink\" node combines these outputs.\n",
    "\n",
    "For example, consider a scenario where you want to sort the outputs of parallel steps based on their \"reliability\"."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "57e475ae",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Annotated, Sequence\n",
    "from typing_extensions import TypedDict\n",
    "from langgraph.graph import StateGraph\n",
    "from langgraph.graph.message import add_messages\n",
    "\n",
    "\n",
    "# Logic to merge fan-out values, handle empty lists, and concatenate lists\n",
    "def reduce_fanouts(left, right):\n",
    "    if left is None:\n",
    "        left = []\n",
    "    if not right:\n",
    "        # Overwrite\n",
    "        return []\n",
    "    return left + right\n",
    "\n",
    "\n",
    "# Type definition for state management, configuring structures for aggregation and fan-out values\n",
    "class State(TypedDict):\n",
    "    # Use the add_messages reducer\n",
    "    aggregate: Annotated[list, add_messages]\n",
    "    fanout_values: Annotated[list, reduce_fanouts]\n",
    "    which: str\n",
    "\n",
    "\n",
    "# Initialize the graph\n",
    "builder = StateGraph(State)\n",
    "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
    "builder.add_edge(START, \"a\")\n",
    "\n",
    "\n",
    "# Class for returning parallel node values\n",
    "class ParallelReturnNodeValue:\n",
    "    def __init__(\n",
    "        self,\n",
    "        node_secret: str,\n",
    "        reliability: float,\n",
    "    ):\n",
    "        self._value = node_secret\n",
    "        self._reliability = reliability\n",
    "\n",
    "    # Update the state when called\n",
    "    def __call__(self, state: State) -> Any:\n",
    "        print(f\"Adding {self._value} to {state['aggregate']} in parallel.\")\n",
    "        return {\n",
    "            \"fanout_values\": [\n",
    "                {\n",
    "                    \"value\": [self._value],\n",
    "                    \"reliability\": self._reliability,\n",
    "                }\n",
    "            ]\n",
    "        }\n",
    "\n",
    "\n",
    "# Add parallel nodes with different reliability values\n",
    "builder.add_node(\"b\", ParallelReturnNodeValue(\"I'm B\", reliability=0.1))\n",
    "builder.add_node(\"c\", ParallelReturnNodeValue(\"I'm C\", reliability=0.9))\n",
    "builder.add_node(\"d\", ParallelReturnNodeValue(\"I'm D\", reliability=0.5))\n",
    "\n",
    "\n",
    "# Aggregate fan-out values based on reliability and perform final aggregation\n",
    "def aggregate_fanout_values(state: State) -> Any:\n",
    "    # Sort by reliability\n",
    "    ranked_values = sorted(\n",
    "        state[\"fanout_values\"], key=lambda x: x[\"reliability\"], reverse=True\n",
    "    )\n",
    "    print(ranked_values)\n",
    "    return {\n",
    "        \"aggregate\": [x[\"value\"][0] for x in ranked_values] + [\"I'm E\"],\n",
    "        \"fanout_values\": [],\n",
    "    }\n",
    "\n",
    "\n",
    "# Add aggregation node\n",
    "builder.add_node(\"e\", aggregate_fanout_values)\n",
    "\n",
    "\n",
    "# Define conditional routing logic based on state\n",
    "def route_bc_or_cd(state: State) -> Sequence[str]:\n",
    "    if state[\"which\"] == \"cd\":\n",
    "        return [\"c\", \"d\"]\n",
    "    return [\"b\", \"c\"]\n",
    "\n",
    "\n",
    "# Configure intermediate nodes and add conditional edges\n",
    "intermediates = [\"b\", \"c\", \"d\"]\n",
    "builder.add_conditional_edges(\"a\", route_bc_or_cd, intermediates)\n",
    "\n",
    "# Connect intermediate nodes to the final aggregation node\n",
    "for node in intermediates:\n",
    "    builder.add_edge(node, \"e\")\n",
    "\n",
    "# Finalize the graph\n",
    "graph = builder.compile()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4a4645d3",
   "metadata": {},
   "source": [
    "Let's visualize the graph."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "61d29bec",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "086416d2",
   "metadata": {},
   "source": [
    "The results from executing nodes in parallel are then sorted based on their reliability.\n",
    "\n",
    "**Reference**\n",
    "\n",
    "- ```b```: reliability = 0.1  \n",
    "- ```c```: reliability = 0.9  \n",
    "- ```d```: reliability = 0.5"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "bff43203",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding I'm A to []\n",
      "Adding I'm B to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='ecf02c68-88bc-469c-b863-182e4daec1bc')] in parallel.\n",
      "Adding I'm C to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='ecf02c68-88bc-469c-b863-182e4daec1bc')] in parallel.\n",
      "[{'value': [\"I'm C\"], 'reliability': 0.9}, {'value': [\"I'm B\"], 'reliability': 0.1}]\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'aggregate': [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='ecf02c68-88bc-469c-b863-182e4daec1bc'),\n",
       "  HumanMessage(content=\"I'm C\", additional_kwargs={}, response_metadata={}, id='a4f32e94-2ff9-43a1-8ae0-e6237979fa5d'),\n",
       "  HumanMessage(content=\"I'm B\", additional_kwargs={}, response_metadata={}, id='e6e20712-91e8-44b2-bb25-9321822d6260'),\n",
       "  HumanMessage(content=\"I'm E\", additional_kwargs={}, response_metadata={}, id='8972efe5-ef49-429a-ab8b-bf08e5487822')],\n",
       " 'fanout_values': [],\n",
       " 'which': 'bc'}"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Execute the Graph (set `which`:`bc`)\n",
    "graph.invoke({\"aggregate\": [], \"which\": \"bc\", \"fanout_values\": []})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "9e2fe2fb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding I'm A to []\n",
      "Adding I'm C to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='44beec0c-8fd5-469f-a705-1101fe4e422a')] in parallel.\n",
      "Adding I'm D to [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='44beec0c-8fd5-469f-a705-1101fe4e422a')] in parallel.\n",
      "[{'value': [\"I'm C\"], 'reliability': 0.9}, {'value': [\"I'm D\"], 'reliability': 0.5}]\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'aggregate': [HumanMessage(content=\"I'm A\", additional_kwargs={}, response_metadata={}, id='44beec0c-8fd5-469f-a705-1101fe4e422a'),\n",
       "  HumanMessage(content=\"I'm C\", additional_kwargs={}, response_metadata={}, id='37cf7718-673c-44ae-912d-f2b92c267faf'),\n",
       "  HumanMessage(content=\"I'm D\", additional_kwargs={}, response_metadata={}, id='6204397b-8bed-4c68-afe0-a5dfd7140aea'),\n",
       "  HumanMessage(content=\"I'm E\", additional_kwargs={}, response_metadata={}, id='588c2b73-b994-44a2-aa5a-d66bac8bacbd')],\n",
       " 'fanout_values': [],\n",
       " 'which': 'cd'}"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Execute the Graph (set `which`:`cd`)\n",
    "graph.invoke({\"aggregate\": [], \"which\": \"cd\"})"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
